Skip to content

Pinned Images#1519

Open
annehaley wants to merge 9 commits into
masterfrom
image-pin-sort
Open

Pinned Images#1519
annehaley wants to merge 9 commits into
masterfrom
image-pin-sort

Conversation

@annehaley

Copy link
Copy Markdown
Contributor

This PR does the following:

  1. Add the pinned boolean field to the Image model
  2. Add an endpoint to set the value of the pinned field on an Image. Only staff users are permitted to set the pinned state.
  3. Add an actions menu with a pin/unpin button to the Image Detail page (created to mimic the actions menu on the Collection Detail page). The pin/unpin button is only visible to staff users. Because the pin/unpin button is the only thing in this menu so far, the whole menu is only visible to staff users. We can remove that extra conditional if other non-staff options are added to the menu.
  4. Change the default sorting of the Image browser page to ("-pinned", "created") so that pinned Images are shown first.
  5. Add a new custom pagination class DynamicOrderCursorPagination, which extends CursorPagination. Using this pagination class allows the user to specify the ordering with the order_by query parameter. The image_list and image_search endpoints now use this pagination class. This will allow us to prioritize pinned images in the gallery by specifying ?order_by=-pinned,created.
  6. Fix a bug in the CursorPagination class, where only the first field in the ordering was honored during pagination.
  7. Add tests for the above behavior (ensure only staff can set pinned state, ensure list and search endpoints can be ordered by pinned state, ensure menu behavior with Playwright)

Comment thread isic/core/templates/core/image_detail/actions_menu.html Outdated
Comment thread isic/core/templates/core/image_detail/actions_menu.html Outdated
Comment thread isic/core/templates/core/image_detail/actions_menu.html Outdated
Comment thread isic/core/api/image.py
response={200: None, 400: dict, 403: dict},
include_in_schema=False,
)
def image_set_pinned(request, id: int, payload: SetPinned):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this just be a form endpoint instead? That's a real question, not a suggestion.

@danlamanna Thoughts?

@annehaley

Copy link
Copy Markdown
Contributor Author

@brianhelba Thanks for the suggestions. I can definitely make these changes, but I'll first ask if we should make the same changes in the following places:

I had copied code from each of these places so I could mimic the pinning behavior of collections. Should we try to keep them the same?

@brianhelba

Copy link
Copy Markdown
Member

Good point. Since this is already getting to be a larger PR, I'd say is actually fine to keep here as-is, then make a follow-up PR to improve all these places.

@annehaley annehaley changed the base branch from master to async-and-errs June 11, 2026 17:55
@annehaley annehaley changed the base branch from async-and-errs to master June 11, 2026 18:51
@danlamanna

Copy link
Copy Markdown
Member

@annehaley It's been a while since I've looked at the cursor pagination implementation but I'd really like to minimize changes to the paginator if at all possible. Is it possible to just order the queryset in the API view based on the query string and let the existing paginator handle it?

As it stands I don't think this is working at the moment. I think mutating state on the paginator is making the server stateful, which is leading to broken behavior e.g. request 1 orders by pinned and request 2 doesn't specify an ordering, but gets the same ordering as request 1. I believe the tests are passing by accident; for instance if I put this block after your first assertion:

    # List endpoint w/ no ordering
    r = authenticated_client.get(reverse("api:image_list"))
    ordered_ids = [image.get("isic_id") for image in r.json().get("results")]
    assert ordered_ids == [image_1.isic_id, image_2.isic_id]

It fails, but if I put it before, then it passes.

I think we actually ought to hardcode this to sorting by our pinned images ordering. If we allow users to arbitrarily order on anything then we run in to the risk of them ordering on something that isn't indexed, which would end up needing to do full table scans repeatedly. In this case, I think we might even be allowing users to introduce joins e.g. ordering by shares which could duplicate results and cause a lot of strain on the database.

So could we figure out how to aggressively pare back these changes, verify the pinned ordering is able to benefit from an index (it may not need a new one?), and see if it can work?

@annehaley

Copy link
Copy Markdown
Contributor Author

Ok, if we can assume that we always want to sort images pin-first then by creation timestamp, we no longer need the DynamicOrderCursorPagination class. It only exists to allow the user to specify the ordering in the query params. I had added this because you had orginally stated that the pin-first sort should be limited to opt-in via the API. Instead, we can just change the ordering declared on the Image model's Meta class.

However, we will still need to change the original CursorPagination class. The way it was built only considers ordering by one field. In these 2 places x, x, the code only considers the field at index 0 in the ordering. 6d95c5d updates the class to consider more than just the first field in the ordering list. Without this change, the paginator will only sort by pinned status without then ordering by creation timestamp.

@annehaley

Copy link
Copy Markdown
Contributor Author

@danlamanna As per our discussion, 3634342 isolates the custom pagination logic to a class called PinnedFirstPagination (open to other name suggestions). This class will still only order by created by default, but if the query contains the pin_sort parameter, then the ordering will be by pinned and then by created. This prevents arbitrary ordering, but still limits the API pin sorting to opt-in behavior. And rather than setting self.ordering, which caused the statefulness bug you identified, this class sets queryset.query.order_by when pin sorting is enabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants